package Schiffeversenken;

import javax.bluetooth.*;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Vector;


/**
 * Initialize BT device, search for BT services,
 * presents them to user and picks his/her choice,
 * finally download the choosen image and present
 * it to user.
 *
 * Do NOT reset BTClient/Server. Instead simply make create a new one
 *
 * @version ,
 */
final class BTClient implements Runnable, DiscoveryListener {
    /**
     * Describes this server
     */
    private static final UUID BATTLESHIPS_SERVER_UUID =
        new UUID ("F0E0D0C0B0A000908070605040302010", false);

    /**
     * Shows the engine is ready to work.
     */
    private static final int READY = 0;

    /**
     * Shows the engine is searching bluetooth devices.
     */
    private static final int DEVICE_SEARCH = 1;

    /**
     * Shows the engine is searching bluetooth services.
     */
    private static final int SERVICE_SEARCH = 2;

    /** This state is set as normal gameplay is running */
    private static final int GAMEPLAY = 3;

    // -------------------------------------------------------------------------
    /** True if player has placed his ships and is ready to duel.
        Set this to true by calling PlayerReady().
     */
    private boolean playerReady = false;
    
    /** True if opponent has placed his ships and is ready to duel */
    private boolean opponentReady = false;

    /** ID of this game session */
    private String sessionId;

    /** This works as a semaphore to indicate whether outputString will be
     * written soon and is not free for read or write.
     */
    private boolean outputInUsageSemaphore = false;

    public Battleground battleground;

    /*
     * Message Codes
     *
     * For each received SHOOT with a certain sequence number,
     * a device has to answer with the same sequence number if
     * its a shot into the WATER, a HIT, or SUNK.
     *
     * General setup:
     * MessageType : String +   // type of message (see codes below)
     * SessionID : String +     // a session unique ID
     * SequenceNumber : int (+  // ID of message
     * X-coord : int +          // optional
     * Y-coord : int)           // optional
     * 
     * e.g: SHOOT+561+3+1  or  HIT+562+3+1  or  PLACED+1
     *
     * BTK + SessionId
     *          // other device is successfully set up and ready + new sessionID
     * RDY    // opponent has placed its ships and is waiting for other player
     * STC    // both players ready - client starts shooting
     * STS    // both players ready - server starts shooting
     * QIT    // player wants to leave the session or timeout
     *
     * SHT    // shot at given coords
     * WTR    // shot with according sequence number dropped into water
     * HIT    // shot with according sequence number hit an enemy ship
     * SNK    // shot with according sequence number hit and destroyed a ship
     *
     * not implemented but perhaps useful:
     * RAL    // request keepalive from other device after timeout of
     *        // unreceived message
     * ALV    // device is still connected
     * */

    public static final String BT_READY = "BTK";
    public static final String PLACEMENT_READY = "RDY";
    public static final String START_CLIENT = "STC";
    public static final String START_SERVER = "STS";
    public static final String QUIT = "QIT";

    public static final String SHOOT = "SHT";
    public static final String WATER = "WTR";
    public static final String HIT = "HIT";
    public static final String SUNK = "SNK";

    public static final String CONCAT = "+";

    /** Try to send a shoot message.
     * @return returns false if message cannot be sent currently, true otherwise
     */
    public boolean Shoot(Position pos)
    {
        /*if (this.state != GAMEPLAY  || this.isClosed ||
                this.outputWaiting || this.outputInUsageSemaphore ||
                !this.battleground.IsOnTurn())
        {
            return false;
        }*/
        this.outputInUsageSemaphore = true;

        // build the message
        String message = SHOOT +
                    CONCAT +
                this.sessionId  +
                    CONCAT +
                this.messageId +
                    CONCAT +
                String.valueOf(pos.X()) +
                    CONCAT +
                String.valueOf(pos.Y())
                ;

        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;
        
        return result;
    }

    /** Call this as player is done placing his ships to send a message */
    public boolean PlacementDone()
    {
        /*if (this.isClosed || this.outputWaiting || this.outputInUsageSemaphore)
        {
            return false;
        }*/
        this.outputInUsageSemaphore = true;

        this.playerReady = true;
        // build the message
        String message = PLACEMENT_READY +
                    CONCAT +
                this.sessionId +
                    CONCAT +
                this.messageId
                ;

        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Call this as BT is set up and ready for use */
    public boolean BTReady()
    {
        /*if (this.isClosed || this.outputWaiting || this.outputInUsageSemaphore)
        {
            return false;
        }*/
        this.outputInUsageSemaphore = true;


        // we are the client so we may haven't obtained a valid sessionId from
        // the server yet. So just send 0 - the server isn't interested in the
        // ID at this time anyway.
        

        // build the message
        String message = BT_READY +
                    CONCAT +
                "0" +
                    CONCAT +
                this.messageId
                ;

        
        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Called as the player wants to leave the match */
    public boolean SendQuit()
    {
        if (this.state != READY)
        {
            // BT not yet initialized
            return true;
        }
        /*if (this.outputWaiting || this.outputInUsageSemaphore)
        {
            return false;
        }*/
        this.outputInUsageSemaphore = true;


        // build the message
        String message = QUIT +
                    CONCAT +
                this.sessionId +
                    CONCAT +
                this.messageId
                ;


        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Send message to opponent if his last shot hit.
     * @param shotResult 0 - water
     *                   1 - hit
     *                   2 - hit and sunk
     * @param pos position the shot aimed at
     */
    public boolean SendShotResult(int shotResult, Position pos)
    {
        /*
        if (this.isClosed || this.outputWaiting || this.outputInUsageSemaphore)
        {
            if (this.outputWaiting)
            {
                System.out.println("output occupied");
            }
            if (this.outputInUsageSemaphore)
            {
                System.out.println("semaphore occupied");
            }
            return false;
        }
         * */
        this.outputInUsageSemaphore = true;


        String hitString = "";
        switch (shotResult)
        {
            case 0:
                hitString = WATER;
                break;
            case 1:
                hitString = HIT;
                break;
            case 2:
                hitString = SUNK;
                break;
        }


        // build the message
        String message = hitString +
                    CONCAT +
                this.sessionId +
                    CONCAT +
                this.messageId +
                    CONCAT +
                pos.X() +
                    CONCAT +
                pos.Y()
                ;


        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Decodes a incoming message and reacts properly */
    public void InterpreteIncomingMessage(String message)
    {
        System.out.println("interpretng started of message " + message);
        if (message == null || message.length() < 3)
        {
            System.err.println("invalid message:");
            if (message == null)
            {
                System.err.println("message not existing");
            }
            else if (message.length() < 3)
            {
                System.err.println("message too short");
            }
            return;
        }

        String messageType;
        String obtainedSessionId;
        int concatIndex, oldConcatIndex;


        // -- read MessageType --
        concatIndex = message.indexOf(CONCAT);
        if (concatIndex == -1)
        {
            System.err.println("failed reading first CONCAT");
            return;
        }
        messageType = message.substring(0, concatIndex);
        System.out.print("reading message. type: " + messageType);
        // -----------------------------


        // -- read sessionID --
        oldConcatIndex = concatIndex + 1;
        concatIndex = message.indexOf(CONCAT, oldConcatIndex);
        if (concatIndex == -1)
        {
            return;
        }
        obtainedSessionId = message.substring(oldConcatIndex, concatIndex);
        System.out.print("sessionID: " + sessionId);
        //System.out.println("-- obtainedID: " + obtainedSessionId + "old:" + oldConcatIndex + "new:" + concatIndex);


        // if this is a BT_READY message we receive the sessionID from the
        // server
        if (messageType.equals(BT_READY))
        {
            // opponent' BT is ready - set new sessionID to given one
            this.sessionId = obtainedSessionId;
            System.out.println("received new sessionID: " + this.sessionId);
            return;
        }
        else
        {
            // check if this is the vaild sessionId
            if (!obtainedSessionId.equals(this.sessionId))
            {
                System.err.println("invalid sessionID: " + obtainedSessionId +
                        " expected: " + this.sessionId);
                return;
            }
        }


        if (messageType.equals(PLACEMENT_READY))
        {
            // opponent has placed his ships
            this.opponentReady = true;
        }
        else if (messageType.equals(QUIT))
        {
            // shut down BT
            this.parent.OpponentQuit();
            this.isClosed = true;
            return;
        }
        else if (messageType.equals(START_CLIENT))
        {
            this.state = GAMEPLAY;
            this.battleground.SetState(1);
            // @todo: tell player to make a move
            return;
        }
        else if (messageType.equals(START_SERVER))
        {
            this.state = GAMEPLAY;
            this.battleground.SetState(2);
            // @todo: tell player to wait until opponent has moved
            return;
        }
        // -----------------------

        // -- read message ID --
        oldConcatIndex = concatIndex + 1;
        concatIndex = message.indexOf(CONCAT, oldConcatIndex);
        if (concatIndex == -1)
        {
            return;
        }
        System.out.print("message ID: " + message.substring(oldConcatIndex, concatIndex));
        // ----------------------

        // -- read coordinates --
        int x, y;
        oldConcatIndex = concatIndex + 1;
        concatIndex = message.indexOf(CONCAT, oldConcatIndex);
        if (concatIndex == -1)
        {
            System.err.println("failed reading X coord");
            return;
        }
        x = Integer.parseInt(message.substring(oldConcatIndex, concatIndex));

        // read the rest
        y = Integer.parseInt(message.substring(concatIndex + 1, message.length()));
        System.out.println("coords: " + x + y);
        // ----------------------


        // -- act properly --
        if (messageType.equals(SHOOT))
        {
            // wait until BG is ready
            while (!this.battleground.readyToReceiveIncoming)
            {
                try {this.wait(250);}
                catch(Exception e){e.printStackTrace();}
            }
            // shoot!
            this.battleground.EnemyShot(new Position(x, y));
        }
        else if (messageType.equals(WATER))
        {
            this.battleground.ReceiveShotResult(0);
        }
        else if (messageType.equals(HIT))
        {
            this.battleground.ReceiveShotResult(1);
        }
        else if (messageType.equals(SUNK))
        {
            this.battleground.ReceiveShotResult(2);
        }
        else
        {
            // whops - something went wrong
            System.err.println("Failed identifying messageType: " + messageType);
            return;
        }
        // ------------------------
    }

    /** ID of the next message to be send. Currently this is correctly written
     * but never read and just existing as a potential problem solver if
     * it shows that BT is unrealiable in message delivery. In that case a list
     * of messageIDs which we wait for approval for must exist.
     * 
     * NOTE: As ID is incremented after building the message-string this ID
     * is the increment of the message, currently in the OutputStream.
     * The ID may change on a higher ID incoming on the InputStream, too.
     */
    int messageId = 0;
    // -------------------------------------------------------------------------

    /**
     * Keeps the current state of engine.
     */
    private int state = READY;

    /**
     * Keeps the discovery agent reference.
     */
    private DiscoveryAgent discoveryAgent;

    /**
     * Keeps the parent reference to process specific actions.
     */
    private GameInstance parent;

    /**
     * Becomes 'true' when this component is finalized.
     */
    private boolean isClosed;

    /**
     * Process the search/download requests.
     */
    private Thread processorThread;

    /**
     * Collects the remote devices found during a search.
     */
    private Vector /* RemoteDevice */ devices = new Vector ();

    /**
     * Collects the services found during a search.
     */
    private Vector /* ServiceRecord */ records = new Vector ();

    /**
     * Keeps the device discovery return code.
     */
    private int discType;

    /**
     * Keeps the services search IDs (just to be able to cancel them).
     */
    private int[] searchIDs;

    /**
     * Keeps the image name to be load.
     */
    private String imageNameToLoad;

    /**
     * Keeps the table of {name, Service} to process the user choice.
     */
    private Hashtable base = new Hashtable ();

    /**
     * Informs the thread the download should be canceled.
     */
    private boolean isDownloadCanceled;

    /**
     * Optimization: keeps service search pattern.
     */
    private UUID[] uuidSet;

    /**
     * Optimization: keeps attributes list to be retrieved.
     */
    private int[] attrSet;

    StreamConnection connection;
    
    boolean outputWaiting = false;
    String messageToSend = "";//MESSAGE FROM CLIENT";

    /**
     * Constructs the bluetooth server, but it is initialized
     * in the different thread to "avoid dead lock".
     * @param parent parent
     */
    BTClient (GameInstance parent) {
        this.parent = parent;

        // we have to initialize a system in different thread...
        processorThread = new Thread (this);
        processorThread.start ();
    }

    private LocalDevice localDevice;
    /**
     * Process the search/download requests.
     */
    public void run () {
        // initialize bluetooth first
        boolean isBTReady = false;

        System.out.println("client start run");
        try {
            // create/get a local device and discovery agent
            this.localDevice = LocalDevice.getLocalDevice ();
            discoveryAgent = this.localDevice.getDiscoveryAgent ();

            // remember we've reached this point.
            isBTReady = true;
        }
        catch (Exception e) {
            System.err.println ("Can't initialize bluetooth: " + e);
        }

        // not needed?
        //parent.BTReady(isBTReady);

        System.out.println("BT is ready: " + String.valueOf(isBTReady));

        // nothing to do if no bluetooth available
        if (!isBTReady) {
            return;
        }

        // initialize some optimization variables
        uuidSet = new UUID[2];

        // ok, we are interesting in btspp services only
        uuidSet[0] = new UUID (0x1101);

        // and only known ones, that allows pictures
        uuidSet[1] = BATTLESHIPS_SERVER_UUID;

        // we need an only service attribute actually
        attrSet = new int[1];

        System.out.println("start processing");
        // start processing the images search/download
        this.EstablishConnection();

        // read / write messages
        while(!isClosed)
        {
            synchronized(this)
            {
                try
                {
                    this.wait(200);
                }
                catch (Exception e)
                {
                    System.err.println(e);
                }
            }
            if (this.outputWaiting && !this.outputInUsageSemaphore)
            {
                if (this.SendMessage(this.messageToSend))
                {
                    // message successfully transmitted
                    this.outputWaiting = false;
                }
            }
            this.ReceiveMessage();
        }
        System.out.println("shutting down server");
    }

    /**
     * Invoked by system when a new remote device is found -
     * remember the found device.
     */
    public void deviceDiscovered (RemoteDevice btDevice, DeviceClass cod) {
        // same device may found several times during single search
        if (devices.indexOf (btDevice) == -1) {
            devices.addElement (btDevice);
        }
    }

    /**
     * Invoked by system when device discovery is done.
     * <p/>
     * Remember the discType
     * and process its evaluation in another thread.
     */
    public void inquiryCompleted (int discType) {
        this.discType = discType;

        synchronized (this) {
            notify ();
        }
    }

    public void servicesDiscovered (int transID, ServiceRecord[] servRecord) {
        for (int i = 0; i < servRecord.length; i++) {
            records.addElement (servRecord[i]);
        }
    }

    public void serviceSearchCompleted (int transID, int respCode) {
        // first, find the service search transaction index
        int index = -1;

        for (int i = 0; i < searchIDs.length; i++) {
            if (searchIDs[i] == transID) {
                index = i;

                break;
            }
        }

        // error - unexpected transaction index
        if (index == -1) {
            System.err.println ("Unexpected transaction index: " + transID);
            // process the error case here
        }
        else {
            searchIDs[index] = -1;
        }

        /*
         * Actually, we do not care about the response code -
         * if device is not reachable or no records, etc.
         */

        // make sure it was the last transaction
        for (int i = 0; i < searchIDs.length; i++) {
            if (searchIDs[i] != -1) {
                return;
            }
        }

        // ok, all of the transactions are completed
        synchronized (this) {
            notify ();
        }
    }

    /**
     * Sets the request to search the devices/services.
     */
    void requestSearch () {
        synchronized (this) {
            notify ();
        }
    }

    /**
     * Cancel's the devices/services search.
     */
    void cancelSearch () {
        synchronized (this) {
            if (state == DEVICE_SEARCH) {
                discoveryAgent.cancelInquiry (this);
            }
            else if (state == SERVICE_SEARCH) {
                for (int i = 0; i < searchIDs.length; i++) {
                    discoveryAgent.cancelServiceSearch (searchIDs[i]);
                }
            }
        }
    }

    /**
     * Sets the request to load the specified image.
     */
    void requestLoad (String name) {
        synchronized (this) {
            imageNameToLoad = name;
            notify ();
        }
    }

    /**
     * Cancel's the image download.
     */
    void cancelLoad () {
        /*
         * The image download process is done by
         * this class's thread (not by a system one),
         * so no need to wake up the current thread -
         * it's running already.
         */
        isDownloadCanceled = true;
    }

    /**
     * Destroy a work with bluetooth - exits the accepting
     * thread and close notifier.
     */
    void destroy () {
        this.SendQuit();
        
        System.out.println("shutting down client");
        synchronized (this) {
            isClosed = true;
            isDownloadCanceled = true;
            notify ();
        }

        // wait for acceptor thread is done
        try {
            processorThread.join ();
        }
        catch (InterruptedException e) {
        } // ignore
    }

    /**
     * Processes images search/download until component is closed
     * or system error has happen.
     */
    private synchronized void EstablishConnection () {
        while (!isClosed) {
            System.out.println("establishing connection");
            // wait for new search request from user
            state = READY;

            /*
            try {
                wait ();
            }
            catch (InterruptedException e) {
                System.err.println ("Unexpected interruption: " + e);

                return;
            }*/

            // check the component is destroyed
            if (isClosed) {
                return;
            }

            System.out.println("starting server search");
            // search for devices
            if (!searchDevices ()) {
                return;
            }
            else if (devices.size () == 0) {
                continue;
            }

            // search for services now
            if (!searchServices ()) {
                return;
            }
            /*
            else if (records.size () == 0) {
                continue;
            }*/

            // we got a server!
            System.out.println("Servers found!");
            for (int i = 0; i < records.size(); i++)
            {
                System.out.println("record " + String.valueOf(i) + ": " + records.elementAt(i));
            }

            if (this.Connect())
            {
                try
                {
                this.input = this.connection.openInputStream();
                this.output = this.connection.openOutputStream();
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }
                
                // connection ready for use!
                this.parent.BTReady();
                
                while(!this.BTReady())
                {
                    try{this.wait(500);}
                    catch (Exception e) {}
                }
                
                break;
            }
        }
    }

    InputStream input;
    OutputStream output;

    public boolean Connect()
    {
        System.out.print("trying to connect to server ...");
        String[] urls = new String[this.records.size()];
        // extract URLs from tracked servers
        for (int i = 0; i < this.records.size(); i++)
        {
            urls[i] = ((ServiceRecord)(this.records.elementAt(i))).getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
        }

        // try to connect to a valid server
        for (int i = 0; i < urls.length; i++) {
            StreamConnection conn;

            // first - connect
            try {
                this.connection = (StreamConnection)Connector.open(urls[i]);
            }
            catch (IOException e) {
                System.err.println ("Note: can't connect to: " + urls[i]);
                System.out.println("failed!");
                // ignore
                continue;
            }

            if (this.connection != null)
            {
                break;
            }
        }
        
        System.out.println(" done!");
        return this.connection != null;
    }

    /**
     * Search for bluetooth devices.
     *
     * @return false if should end the component work.
     */
    private boolean searchDevices () {
        // ok, start a new search then
        state = DEVICE_SEARCH;
        devices.removeAllElements ();

        try {
            discoveryAgent.startInquiry (DiscoveryAgent.GIAC, this);
        }
        catch (BluetoothStateException e) {
            System.err.println ("Can't start inquiry now: " + e);
            parent.informSearchError ("Can't start device search");

            return true;
        }

        try {
            wait (); // until devices are found
        }
        catch (InterruptedException e) {
            System.err.println ("Unexpected interruption: " + e);

            return false;
        }

        // this "wake up" may be caused by 'destroy' call
        if (isClosed) {
            return false;
        }

        // no?, ok, let's check the return code then
        switch (discType) {
        case INQUIRY_ERROR:
            parent.informSearchError ("Device discovering error...");

            // fall through
        case INQUIRY_TERMINATED:
            // make sure no garbage in found devices list
            devices.removeAllElements ();

            // nothing to report - go to next request
            break;

        case INQUIRY_COMPLETED:

            if (devices.size () == 0) {
                parent.informSearchError ("No devices in range");
            }

            // go to service search now
            break;

        default:
            // what kind of system you are?... :(
            System.err.println ("system error:" + " unexpected device discovery code: " + discType);
            destroy ();

            return false;
        }

        return true;
    }

    /**
     * Search for proper service.
     *
     * @return false if should end the component work.
     */
    private boolean searchServices () {
        state = SERVICE_SEARCH;
        records.removeAllElements ();
        searchIDs = new int[devices.size ()];

        boolean isSearchStarted = false;

        for (int i = 0; i < devices.size (); i++) {
            RemoteDevice rd = (RemoteDevice) devices.elementAt (i);

            try {
                searchIDs[i] = discoveryAgent.searchServices (attrSet, uuidSet, rd, this);
            }
            catch (BluetoothStateException e) {
                System.err.println (
                    "Can't search services for: " + rd.getBluetoothAddress () +
                        " due to " + e);
                searchIDs[i] = -1;

                continue;
            }

            isSearchStarted = true;
        }

        // at least one of the services search should be found
        if (!isSearchStarted) {
            parent.informSearchError ("Can't search services.");

            return true;
        }

        try {
            wait (); // until services are found
        }
        catch (InterruptedException e) {
            System.err.println ("Unexpected interruption: " + e);

            return false;
        }

        // this "wake up" may be caused by 'destroy' call
        if (isClosed) {
            return false;
        }

        // actually, no services were found
        if (records.size () == 0) {
            parent.informSearchError ("No proper services were found");
        }

        return true;
    }

    private boolean SendMessage(String message)
    {
        System.out.println("client trying to send: " + message);
        // then open a steam and write a name
        try {
            System.out.print("writing length ... ");
            output.write (message.length () >> 8); // first length byte
            output.write (message.length() & 0xff);
            System.out.println("done!");

            System.out.print("writing data ... ");
            output.write (message.getBytes ());
            System.out.println("done!");

            System.out.print("flushing output ... ");
            output.flush ();
            //output.close();
            System.out.println("done!");
            //out.close ();
        }
        catch (IOException e) {
            System.err.println ("Sending message failed: " + message + e.getMessage());
            return false;
        }

        System.out.println("succeeded!");
        return true;
    }

    private void ReceiveMessage()
    {
        // at very first check if something is available on input stream
        try
        {
            if (input.available() == 0)
            {
                return;
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }


        // then read length of message (which may be unuequal to current
        // number of digits in the inputstream as some digits may be 'on the way'
        int length;
        try
        {
            length = input.read() << 8;
            length |= input.read();
        }
        catch (Exception e)
        {
            System.out.println("failed to read LENGTH from input stream");
            return;
        }

        if (length <= 0)
        {
            return;
        }

        System.out.println("reading message of length: " + String.valueOf(length));


        // read data
        byte[] buffer = new byte[length];
        length = 0;
        try
        {
            while (length != buffer.length) {
                int n = input.read (buffer, length, buffer.length - length);

                if (n == -1) {
                    throw new IOException ("failed to read message");
                }

                length += n;
            }

            // write message to input buffer
            String messageString = new String(buffer /*, "utf-8"*/);
            System.out.println("read BT message: " + messageString);
            this.InterpreteIncomingMessage(messageString);

            //input.close ();
        }
        catch (IOException e)
        {
            System.err.println ("Reading message failed");
            return;
        }
    }
}
